/* Hiliter.js library : permanently highlight text in a document.
Using TreeWalker, Firefox only.

Usage :

-Create a Hiliter Object (h) with the window to highlight as parameter. 
-Give a range to the Hiliter to highlight (h.hiliteRange(YourRange))
-To delete a highlight, give the hilite attribute to remove (h.removeRange("hilite"))
-To change the default highlighting style pass an element node (without children) with class hilited and your style to setStyleNode (h.setStyleNode(YourNode))
-To change annotation pass an element node with class hiliteAnnotation and it's style / content (%%... tag) to setStyleNode
-To create an annotation use createAnnotation with the corresponding id and tag value (h.createAnnotation(YourId, {"YourTag":"tag_value", "YourTag2":"tag_value2", ...})
-Get the content of an hilited id with hiliteContent (h.hiliteContent(YourId))
-Get all nodes corresponding to an hilite id with getElementsByHilite (h.getElementsByHilite(YourId))
-Export / Import the history as a String with exportHistory() / importHistory(historyString) Hiliter's methods
*/

//Hiliter class
function Hiliter(win){

//public vars :
	this.history = new Array(0);								//Hiliter's History, removed highlight = null

//private vars : 
	var w = win;												//window reference
	var pointer = 0;											//Current hilite attribute
	var order = 0;												//Operation order
	var styleNode = document.createElement('span'); 			//Range default surrounding node
	styleNode.style.backgroundColor = "yellow";
	styleNode.className = "hilited";
	
	var annotation = null;										//Tooltip node
	var annotations = new Array();								//ToolTip nodes array
	var annotationContainer = w.document.createElement("div"); 	//Used to append annotation
	annotationContainer.style.position = "absolute";
	annotationContainer.style.left = "0px";
	annotationContainer.style.top = "0px";
	w.document.body.appendChild(annotationContainer);
	
	
//public functions :

	//Used to highlight range in parameter return hilite id
	//Update history
	this.hiliteRange = function(range, imp){
		if(imp == null) imp = false;
		styleNode.setAttribute("hilite", pointer);
		this.history.push(new Array(position(range.startContainer, pointer), range.startOffset, position(range.endContainer, pointer), range.endOffset, order));
		pointer++;
		order++;
		var rArray = new Array(0);
		if (range.startContainer === range.endContainer){					//Only one selected node
			rArray.push(range);
		}else {
			rArray = splitRange(range);
		}
		
		for(var i = 0 ; i < rArray.length ; i++){
			rArray[i].surroundContents(styleNode.cloneNode(false));
		}
		checkBorder(new Array([pointer -1]));
		if(imp) this.createAnnotation(pointer - 1, imp);
		return (pointer - 1);
	}
	
	//Change highlight style with new surrounding node or change annotation node
	this.setStyleNode = function(node){
		if(node.nodeType == Node.ELEMENT_NODE){
			if(node.className == "hiliteAnnotation"){
				annotation = node;
				return true;
			}else if(node.className == "hilited"){
				styleNode = node;
				return true;
			}
		}else return false;
	}
	
	//Remove highlighted node with hilite attribute as parameter
	//Recompute highlighted index of children
	//Recompute highlighted index of next Siblings with their children
	//Smooth new subtree
	//Remove corresponding annotation
	this.removeRange = function(identification){
		var elToClean = this.getElementsByHilite(identification);
		var toRecompute = new Array(0);
		for (var i = 0 ; i < elToClean.length; i++){
			var content = elToClean[i].childNodes;
			var elParent = elToClean[i].parentNode;
			for(var c = 0 ; c < content.length ; c++){
				if(content[c].nodeType != Node.TEXT_NODE ){
					if(content[c].className == "hilited"){
						var recompute = content[c].getAttribute("hilite");
						var newVal = true;
						for(var r=0; r < toRecompute.length; r++){
							if(toRecompute[r] == recompute){
								newVal = false;
								break;
							}
						}
						if(newVal) toRecompute.push(recompute);						
					}
				}
				elParent.insertBefore(content[c].cloneNode(true), elToClean[i]);
			}
			var nextSiblings =  indexModificationOnNextSiblings(elToClean[i]);
			for(var s = 0; s < nextSiblings.length;s++){
				if(toRecompute.length == 0 && nextSiblings.length != 0)toRecompute.push(nextSiblings[0]);
				for(var r = 0; r < toRecompute.length; r++){
					if(nextSiblings[s] == toRecompute[r]) break;
					if(r == toRecompute.length - 1) toRecompute.push(nextSiblings[s]);
				}
			}
			elParent.removeChild(elToClean[i]);
			smoothChildren(elParent);
		}
		this.history[identification] = null;
		if(annotations[identification]){
			//this.toggleAnnotation(identification);
			annotations[identification] = null;
		}
		if(toRecompute.length >0){
			for(var i=0; i < toRecompute.length; i++){
				if(this.history[toRecompute[i]][5]){
					var tmpTT = this.history[toRecompute[i]][5];
					this.history[toRecompute[i]] = recomputeHistory(toRecompute[i]);
					this.history[toRecompute[i]][5] = tmpTT;
				}else this.history[toRecompute[i]] = recomputeHistory(toRecompute[i]);	
			}
		}
		checkBorder(toRecompute);
	}
	
	//Return highlighted text content
	this.hiliteContent= function(id){
		var h = this.getElementsByHilite(id);
		var content = "";
		for(var i=0; i<h.length; i++){
			content += h[i].textContent;
		}
		return content;
	}
	
	//Annotation creator (parser replace %%... values)
	this.createAnnotation = function (id, datas){
		if(annotation == null) return false;
		var t = annotation.cloneNode(true);
		t.setAttribute("hilite", id);
		var walker = w.document.createTreeWalker(t, NodeFilter.SHOW_TEXT, filter, true);
		do{
			if(walker.currentNode.nodeType == Node.TEXT_NODE){
				var cur = walker.currentNode.textContent;
				for (var i in datas){
					var repTag = new RegExp("%%"+i, "g");
					cur = cur.replace(repTag, datas[i]);
				}
				walker.currentNode.textContent = cur;
			}
		}while(walker.nextNode());
		annotations[id] = t;
		this.history[id][5] = datas;
		return t;
	}
	
	//Geter annotation
	this.getAnnotation = function(id){
		return annotations[id];
	}
	
	//Export history as String without deleted selections
	this.exportHistory = function(){
		var h = new Array(0);
		for(var i = 0; i < this.history.length; i++){
			if(this.history[i] != null) {
				h.push(this.history[i]);	
			}
		}
		return h.toSource();
	}
	
	//Import String history and highlight it
	//Return number of operation done previously on this history
	this.importHistory = function(histo){
		var toHilite = new Array();
		toHilite = eval(histo).sort(sortHistory);
		for(var i=0; i < toHilite.length; i++){
			var r = w.document.createRange();
			r.setStart(elementAtIndex(toHilite[i][0]), toHilite[i][1]);
			r.setEnd(elementAtIndex(toHilite[i][2]), toHilite[i][3]);
			if(toHilite[i][5]) this.hiliteRange(r, toHilite[i][5]);
			else this.hiliteRange(r);
			order = -1;
			order = Math.max(order, toHilite[i][4]);
		}
		order++;
		return (order - 1);
	}
	
	//return array of nodes with hilite attribute id
	this.getElementsByHilite = function(id){
		var taged = w.document.getElementsByTagName(styleNode.tagName);
		var hilited = new Array(0);
		for(var i = 0; i < taged.length; i++){
			if(taged[i].getAttribute('hilite') == id && taged[i].className == "hilited"){
				hilited.push(taged[i]);
			}
		}
		return hilited;
	}


//private functions :

	//Split range to return an array of new ranges of text node (avoid to much tree modification)
	function splitRange(range){
		var tmpA = new Array(0);
		var anchor = w.document.createRange();
		anchor.setStart(range.startContainer, range.startOffset);
		try{
			anchor.setEnd(range.startContainer, range.startContainer.length);
		}catch(e){
			anchor.selectNode(range.startContainer);
		}
		if(anchor.toString().length > 0) tmpA.push(anchor);
		var offset = w.document.createRange();
		offset.setStart(range.endContainer, 0);
		offset.setEnd(range.endContainer, range.endOffset);
		
		var walker = w.document.createTreeWalker(w.document, NodeFilter.SHOW_TEXT, filter, true);
		while (walker.nextNode()){
			if(rangeIntersectsNode(range, walker.currentNode) && walker.currentNode != range.startContainer && walker.currentNode != range.endContainer){
				var r = w.document.createRange();
				try{
					r.setStart(walker.currentNode, 0);
					r.setEnd(walker.currentNode, walker.currentNode.length);
				}catch(e){
					r.selectNodeContents(walker.currentNode);
				}
				if(r.toString().length > 0) tmpA.push(r);
			}
		}
		if(offset.toString().length > 0) tmpA.push(offset);
		return tmpA;
	}
	
	//Tree walker filter
	function filter(node){
		if (node.nodeType == Node.TEXT_NODE && !(/[^\t\n\r ]/.test(node.nodeValue))){
	     		return NodeFilter.FILTER_REJECT;
	   	}else{
	   		return NodeFilter.FILTER_ACCEPT;
	   	}
	}
	
	//Compute index of highlighted node, return new history element
	function recomputeHistory(index){
		var r = getElementsByHilite(index);
		var startNode = r[0];
		var startOffset = 0;
		var endNode = r[r.length-1];
		var endOffset= r[r.length-1].textContent.length;
		if(r[0].parentNode.firstChild != r[0] && r[0].previousSibling.nodeType == Node.TEXT_NODE){
			startNode = r[0].previousSibling;
			startOffset = r[0].previousSibling.textContent.length;
		}
		if(r[r.length-1].parentNode.firstChild != r[r.length-1] && r[r.length-1].previousSibling.nodeType == Node.TEXT_NODE){
			endNode = r[r.length-1].previousSibling;
			endOffset += r[r.length-1].previousSibling.textContent.length;
		}
		order++;
		return new Array(position(startNode, index), startOffset, position(endNode, index), endOffset, (order -1));
	}
	
	//Check if range node belongs to range
	function rangeIntersectsNode(range, node) {
		  var nodeRange = node.ownerDocument.createRange();
		  try {
		    nodeRange.selectNode(node);
		  }
		  catch (e) {
		    nodeRange.selectNodeContents(node);
		  }
		  return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 && range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
	}
	
	//Compute index of node el, exclude node with hilite attribute id
	//Index begins under <body> node
	function position(el, id){
		var list = el.parentNode.childNodes;
		var modificator = 0;
		for(var i=0 ; i<list.length ; i++){
			if(list[i] == el){
				if(el.parentNode == el.ownerDocument.body) return (i - modificator);
				else return position(el.parentNode, id) + "." + (i - modificator);
			}else if(list[i].nodeType == Node.ELEMENT_NODE && list[i].className == "hilited" && list[i].getAttribute("hilite") == id){
				if(list[i] != list[i].parentNode.firstChild && list[i] != list[i].parentNode.lastChild){
					if(list[i].previousSibling.nodeType == Node.TEXT_NODE && list[i].nextSibling.nodeType == Node.TEXT_NODE){
						modificator += 2;
					}
				}else{
					modificator++;
				}
			}
		}
	}
	
	//return node at position pos
	function elementAtIndex(pos){
		var p = pos.toString().split('.');
		var n = w.document.body;
		for (var i=0; i < p.length ; i++){
			n = n.childNodes[p[i]];
		}
		return n;
	}
	
	//Smooth children of node, ie merge text nodes and highlighted nodes
	function smoothChildren(node){
		var children = node.childNodes;
		for (var i = 0; i < children.length; i++){
			if(children[i].nodeType == Node.TEXT_NODE && children[i].textContent == ""){
				node.removeChild(children[i]);
				i--;
			}
		}
		for (var i=0; i < children.length ; i++){
			if(children[i].nodeType == Node.TEXT_NODE && children[i] != node.lastChild){
				if(children[i+1].nodeType == Node.TEXT_NODE){
					children[i].textContent += children[i+1].textContent;
					node.removeChild(children[i+1]);
					i--;
				}
			}else if(children[i].className == "hilited" && children[i] != node.lastChild){
				var h = children[i].getAttribute("hilite");
				if(children[i+1].className == "hilited" && children[i+1].getAttribute("hilite") == h){
					children[i].innerHTML += children[i+1].innerHTML;
					node.removeChild(children[i+1]);
					smoothChildren(children[i]);
					i--;
				}
			}
		}
	}
	
	//return highlighted next siblings and corresponding children 
	function indexModificationOnNextSiblings(el){
		var id = el.getAttribute("hilite");
		var sibling = el;
		var toRec = new Array(0);
		while(sibling != el.parentNode.lastChild){
			sibling = sibling.nextSibling;
			var sWalker = w.document.createTreeWalker(sibling, NodeFilter.SHOW_ELEMENT, filter, true);
			do{
				if(sWalker.currentNode.nodeType == Node.ELEMENT_NODE && sWalker.currentNode.hasAttribute("hilite") && sWalker.currentNode.getAttribute("hilite") != id){
					toRec.push(sWalker.currentNode.getAttribute("hilite"));
				}
			}while(sWalker.nextNode());
		}
		return toRec;
	}
	
	//return array of nodes with hilite attribute id
	function getElementsByHilite(id){
		var taged = w.document.getElementsByTagName(styleNode.tagName);
		var hilited = new Array(0);
		for(var i = 0; i < taged.length; i++){
			if(taged[i].getAttribute('hilite') == id && taged[i].className == "hilited"){
				hilited.push(taged[i]);
			}
		}
		return hilited;
	}
	
	//Check if border exists to let left(right) border only on most left(right) node
	function checkBorder(idA){
		var lb = styleNode.style.borderLeft;
		var rb = styleNode.style.borderRight;
		for(var i = 0; i < idA.length; i++){
			var rArray = getElementsByHilite(idA[i]);
			if(rArray.length == 1){
				rArray[0].style.borderLeft = lb;
				rArray[0].style.borderRight = rb;
			}
			else{
				for(var i = 0 ; i < rArray.length ; i++){
					if(i == 0) rArray[i].style.borderRight = "none";	
					else if(i == rArray.length - 1) rArray[i].style.borderLeft = "none";
					else{
						rArray[i].style.borderLeft = "none";
						rArray[i].style.borderRight = "none";
					}
				}
			}
			for(var i = 0 ; i < rArray.length ; i++){				//(refresh) Used to correctly apply style (Firefox)
				if(rArray[i].parentNode){
					var tmpC = rArray[i].parentNode.innerHTML;
					rArray[i].parentNode.innerHTML = tmpC;
				}
			}
		}
	}
	
	//Used to sort history by order of operations
	function sortHistory(a, b){
		return a[4] - b[4];
	}
	
}


